﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WMM.Tapioca.Taxonomy;
using WMM.Tapioca.Instance;
using WMM.Tapioca.Linkbase;
using System.Xml;
using System.Xml.Schema;
using System.Net;
using System.Collections;
using System.Xml.Linq;
using WMM.Tapioca.Utilities;

namespace WMM.Tapioca.Validate
{
    /// <summary>
    /// Class that validates the DTS.
    /// </summary>
    public class ValidateDts
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private DTS dts;

        /// <summary>
        /// constructor.
        /// </summary>
        public ValidateDts(DTS dtsXbrl)
        {
            log.Info("Validating Schemas, count:" + dtsXbrl.TaxonomySchemas.Count);
            Controller.Instance.RaiseModelChange(this, new ModelChangeEventArgs("Validating Schemas, count:" + dtsXbrl.TaxonomySchemas.Count));

            dts = dtsXbrl;
            ValidateSchemas();
            ValidateConcepts();
            ValidateRoleRefAndArcRefInLinkbase();
            ValidateRoleTypeAndArcRoleType();
        }

        XmlSchemaSet xmlSchemaSet = new XmlSchemaSet();

        private void ValidateSchemas()
        {
            foreach (TaxonomySchema taxonomySchema in dts.TaxonomySchemas)
            {
                dts.VerifySchemasDuplicates(xmlSchemaSet, taxonomySchema.XmlSchemaSet);
                xmlSchemaSet.Add(taxonomySchema.XmlSchema);                   
            }

            foreach (TaxonomySchema currentSchema in this.dts.TaxonomySchemas)
            {
                if (!IsNamespaceDefault(currentSchema.TargetNamespace))
                {
                    Validate(currentSchema);
                }
            }
        }


        private void Validate(TaxonomySchema currentSchema)
        {
            // Redefine element MUST NOT be used.

            IEnumerable<XElement> elements = currentSchema.ThisXDocument.Root.Elements();

            foreach (XElement element in elements)
            {
                if (element.Name.LocalName.Equals("redefine"))
                {
                    log.Error("Validation error on schema: Redefine element MUST NOT be used.");
                    dts.ValidatedSchema = false;
                }
            }

            ValidateSchemaInstance(currentSchema);
        }


        private void ValidateSchemaInstance(TaxonomySchema currentSchema)
        {
            ValidationEventHandler eventHandler = new ValidationEventHandler(ValidationHandler);

            try
            {
                RemoveSchemaDuplicate(xmlSchemaSet);

                log.Debug("Parsing schema: " + currentSchema.FileName + " targetNamespace:" + currentSchema.TargetNamespace);
                Controller.Instance.RaiseModelChange(this, new ModelChangeEventArgs("Parsing schema: " + currentSchema.FileName + " targetNamespace:" + currentSchema.TargetNamespace));
                currentSchema.ThisXDocument.Validate(xmlSchemaSet, eventHandler, true);
            }
            catch (Exception ex)
            {
                log.Error("Validation error on schema: " + currentSchema.FileName + "." + ex.Message);
                dts.ValidatedSchema = false;
            }
        }


        private void ValidationHandler(object sender, ValidationEventArgs e)
        {
            IXmlLineInfo lineInfo = ((sender as XObject) as IXmlLineInfo);

            string name =dts. GetFileName((sender as XObject).BaseUri);

            log.Error("Validation error on schema \"" + name + "\", line " + lineInfo.LineNumber + ", position " + lineInfo.LinePosition + ". " + e.Message);
            dts.ValidatedSchema = false;
        }


        private static void RemoveSchemaDuplicate(XmlSchemaSet xmlSchemaSet)
        {
            // Using an ArrayList lets use indexing for accessing the schemas.
            ArrayList schemaList = new ArrayList(xmlSchemaSet.Schemas());

            // ArrayList holding the duplicate schemas.
            ArrayList duplicateSchemaList = new ArrayList();

            for (int i = 0; i < schemaList.Count; i++)
            {
                // Exhaustively test for duplicate schemas by means of the
                // SourceUri property.
                for (int j = i + 1; j < schemaList.Count; j++)
                {
                    if (((XmlSchema)schemaList[i]).TargetNamespace ==
                        ((XmlSchema)schemaList[j]).TargetNamespace)
                    {
                        //duplicateSchemaList.Add((XmlSchema)schemaList[j]);
                        xmlSchemaSet.Remove((XmlSchema)schemaList[j]);
                    }
                }
            }
        }

        private void ValidateConcepts()
        {
            foreach (TaxonomySchema currentSchema in this.dts.TaxonomySchemas)
            {
                ValidateConcepts(currentSchema.Concepts);
            }
        }


        private void ValidateRoleTypeAndArcRoleType()
        {
            ValidateRoleURIAnfArcRoleURI();

            // XBRL 2.1 Specification 5.1.3.4  
            // Standard extended link elements and standard resource elements that use the defined role type MUST be 
            // identified with a usedOn element in the roleType element. 
            // Note that custom extended link elements and custom resource elements are not governed by this constraint.

            ValidateUsedOnElementInRoleType();

            // Within a arcRoleType element there MUST NOT be s-equal usedOn elements.

            ValidateUsedOnElementInArcRoleType();
        }        


        private void ValidateRoleURIAnfArcRoleURI()
        {
            // RoleType
            foreach (RoleType roleType in dts.RoleTypes)
            {
                if (roleType.RoleUri == null)
                {
                    log.Error("Validation error on schema: RoleType RoleURI is missing. RoleType: " + roleType.Definition);
                    dts.ValidatedSchema = false;
                }
            }

            // ArcRoleType
            foreach (ArcRoleType arcRoleType in dts.ArcRoleTypes)
            {
                if (arcRoleType.ArcRoleUri == null)
                {
                    log.Error("Validation error on schema: ArcRoleURI is missing. ArcRoleType: " + arcRoleType.Definition);
                    dts.ValidatedSchema = false;
                }
            }
        }


        private void ValidateUsedOnElementInRoleType()
        {
            foreach (RoleType roleType in dts.RoleTypes)
            {
                List<XName> usedOns = roleType.UsedOnReferences;

                foreach (LinkbaseDocument linkbase in dts.LinkbaseDocuments)
                {
                    bool found = HasRoleRefInLinkbase(roleType, linkbase);

                    if (found)
                    {
                        foreach (ExtendedLink extendedLink in linkbase.ExtendedLinks)
                        {
                            if (extendedLink.ExtendedLinkRole.Equals(roleType.RoleUri.ToString()))
                            {
                                if (!usedOns.Contains(XName.Get(extendedLink.LocalName, extendedLink.NamespaceUri)))
                                {
                                    log.Error(linkbase.FileName + ": A new roleType definition contains a usedOn element pointing it in a different element. RoleType: " + roleType.Definition);
                                    dts.ValidatedSchema = false;
                                }
                            }

                            foreach (Resource resource in extendedLink.Resources)
                            {
                                if (resource is Label)
                                {
                                    Label label = resource as Label;

                                    if (resource.Role.Equals(roleType.RoleUri.ToString()))
                                    {
                                        if (!usedOns.Contains(XName.Get("label", resource.NamespaceUri)))
                                        {
                                            log.Error(linkbase.FileName + ": A new roleType definition contains a usedOn element pointing it in a different element. RoleType: " + roleType.Definition);
                                            dts.ValidatedSchema = false;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Within a roleType element there MUST NOT be s-equal usedOn elements.

            foreach (RoleType roleType in dts.RoleTypes)
            {
                for (int i = 0; i < roleType.UsedOnReferences.Count; i++)
                {
                    for (int j = 0; j < roleType.UsedOnReferences.Count; j++)
                    {
                        if (i != j)
                        {
                            //if (roleType.UsedOnReferences[i].Equals(roleType.UsedOnReferences[j]))
                            if (roleType.UsedOnReferences[i].LocalName.Equals(roleType.UsedOnReferences[j].LocalName) &&
                                roleType.UsedOnReferences[i].Namespace.Equals(roleType.UsedOnReferences[j].Namespace))
                            {
                                log.Error("Validation error: Within a roleType element there MUST NOT be s-equal usedOn elements. UsedOn: " + roleType.UsedOnReferences[i]);
                                dts.ValidatedSchema = false;
                            }
                        }
                    }
                }
            }
        }


        private void ValidateUsedOnElementInArcRoleType()
        {
            foreach (ArcRoleType arcRoleType in dts.ArcRoleTypes)
            {
                for (int i = 0; i < arcRoleType.UsedOnReferences.Count; i++)
                {
                    for (int j = 0; j < arcRoleType.UsedOnReferences.Count; j++)
                    {
                        if (i != j)
                        {
                            //if (arcRoleType.UsedOnReferences[i].Equals(arcRoleType.UsedOnReferences[j]))
                            if (arcRoleType.UsedOnReferences[i].LocalName.Equals(arcRoleType.UsedOnReferences[j].LocalName) &&
                                arcRoleType.UsedOnReferences[i].Namespace.Equals(arcRoleType.UsedOnReferences[j].Namespace))
                            {
                                log.Error("Validation error: Within a arcRoleType element there MUST NOT be s-equal usedOn elements. UsedOn: " + arcRoleType.UsedOnReferences[i]);
                                dts.ValidatedSchema = false;
                            }
                        }
                    }
                }
            }
        }


        private static bool HasRoleRefInLinkbase(RoleType roleType, LinkbaseDocument linkbase)
        {
            IEnumerable<XElement> rolesRefNodes = from c in linkbase.ThisXDocument.Root.Elements()
                                                  where c.Name.LocalName.Equals("roleRef")
                                                  select c;

            foreach (XElement element in rolesRefNodes)
            {
                string roleURI = (string)element.Attribute("roleURI") ?? string.Empty;

                if (roleURI.Equals(roleType.RoleUri.ToString()))
                {
                    return true;
                }
            }

            return false;
        }


        private void ValidateRoleRefAndArcRefInLinkbase()
        {
            foreach (LinkbaseDocument linkbase in dts.LinkbaseDocuments)
            {
                //RoleRef
                IEnumerable<XElement> rolesRefNodes = from c in linkbase.ThisXDocument.Root.Elements()
                                                      where c.Name.LocalName.Equals("roleRef")
                                                      select c;

                XNamespace ns = Constants.XLINK;

                foreach (XElement element in rolesRefNodes)
                {                    
                    string roleURI = (string)element.Attribute("roleURI") ?? string.Empty;

                    foreach (XElement elementToCompare in rolesRefNodes)
                    {
                        if (element != elementToCompare)
                        {
                            string roleURITocompare = (string)elementToCompare.Attribute("roleURI") ?? string.Empty;

                            if (roleURI.Equals(roleURITocompare))
                            {
                                log.Error("Validation error on schema: Schema contains two role references to the same URI in the same linkbase. RoleURI: " + roleURI);
                                dts.ValidatedSchema = false;
                            }

                        }
                    }                    
                }

                //ArcRoleRef
                IEnumerable<XElement> arcRolesRefNodes = from c in linkbase.ThisXDocument.Root.Elements()
                                                         where c.Name.LocalName.Equals("arcroleRef")
                                                         select c;


                foreach (XElement element in arcRolesRefNodes)
                {
                    string arcroleURI = (string)element.Attribute("arcroleURI") ?? string.Empty;

                    foreach (XElement elementToCompare in arcRolesRefNodes)
                    {
                        if (element != elementToCompare)
                        {
                            string arcroleURITocompare = (string)elementToCompare.Attribute("arcroleURI") ?? string.Empty;

                            if (arcroleURI.Equals(arcroleURITocompare))
                            {
                                log.Error("Validation error on schema: Schema contains two arcRole references to the same URI in the same linkbase. ArcRoleURI: " + arcroleURI);
                                dts.ValidatedSchema = false;
                            }

                        }
                    }
                }
            }
        }


        private void ValidateConcepts(IndexedList<Concept> concepts)
        {
            foreach (Concept concept in concepts)
            {
                if ((concept.SubstitutionGroup == Concept.EnumSubstitutionGroup.Item) && concept.PeriodTypeString == null)
                {
                    log.Error("Validation error on schema: The periodType attribute is required for both numeric and non-numeric item types. Concept: " + concept.Name);
                    dts.ValidatedSchema = false;
                }

                if (concept.SubstitutionGroup == Concept.EnumSubstitutionGroup.Tuple)
                {
                    ValidateTuples(concept);
                }

                // An item type in a taxonomy can never have complex content unless it is derived by restriction from fractionItemType.

                if (concept.SubstitutionGroup == Concept.EnumSubstitutionGroup.Item && !concept.TypeName.Name.Equals("fractionItemType") &&
                     concept.SchemaElement.ElementSchemaType is XmlSchemaComplexType)
                {
                    XmlSchemaComplexType complexType = concept.SchemaElement.ElementSchemaType as XmlSchemaComplexType;

                    if (complexType.ContentModel == null)
                    {
                        log.Error("Validation error on schema: An item type in a taxonomy can never have complex content unless it is derived by restriction from fractionItemType. Concept: " + concept.Name);
                        dts.ValidatedSchema = false;
                    }

                    if (complexType.ContentModel != null && complexType.ContentModel is XmlSchemaComplexContent && !concept.SchemaElement.ElementSchemaType.QualifiedName.Name.Equals("fractionItemType"))
                    {
                        log.Error("Validation error on schema: An item type in a taxonomy can never have complex content unless it is derived by restriction from fractionItemType. Concept: " + concept.Name);
                        dts.ValidatedSchema = false;
                    }
                }

                if (!concept.Type.IsNumeric())
                {
                    if (!string.IsNullOrEmpty(concept.BalanceString))
                    {
                        log.Error("Validation error on schema: Derived element with non numeric datatype with Balance defined. Concept: " + concept.Name);
                        dts.ValidatedSchema = false;
                    }
                }
            }
        }

        // XBRL 2.1 Specification 4.9

        private void ValidateTuples(Concept concept)
        {
            // The periodTyope attribute is not allowed on tuples.

            if (concept.PeriodTypeString != null)
            {
                log.Error("Validation error on schema: The periodTyope attribute is not allowed on tuples. Concept: " + concept.Name);
                dts.ValidatedSchema = false;
            }

            // Tuples should not have attributes in XBRL spec defined namespaces.

            if (concept.SchemaElement.ElementSchemaType is XmlSchemaComplexType)
            {
                XmlSchemaComplexType complexType = concept.SchemaElement.ElementSchemaType as XmlSchemaComplexType;

                if (complexType.AttributeUses.Count > 0)
                {
                    IDictionaryEnumerator enumerator =
                        complexType.AttributeUses.GetEnumerator();

                    while (enumerator.MoveNext())
                    {
                        XmlSchemaAttribute attribute = (XmlSchemaAttribute)enumerator.Value;

                        if (attribute.QualifiedName.Namespace.Equals("http://www.xbrl.org/2003/instance") ||
                            attribute.QualifiedName.Namespace.Equals("http://www.w3.org/1999/xlink") ||
                            attribute.QualifiedName.Namespace.Equals("http://www.xbrl.org/2003/linkbase") ||
                            attribute.QualifiedName.Namespace.Equals("http://www.xbrl.org/2003/XLink"))
                        {
                            log.Error("Validation error on schema: Tuples should not have attributes in XBRL spec defined namespaces. Concept: " + concept.Name);
                            dts.ValidatedSchema = false;
                        }
                    }
                }

                //  Tuples MUST NOT have mixed content, or simple content.

                if (complexType.IsMixed)
                {
                    log.Error("Validation error on schema: Tuples must not use the mixed content, or simple content. Concept: " + concept.Name);
                    dts.ValidatedSchema = false;
                }

                // In the declaration of any tuple in a taxonomy schema,
                // declarations of child elements of that tuple MUST be references to global
                // element declarations that are in a substitution group that has either item
                // or tuple as its head.

                // Get the sequence particle of the complex type.
                XmlSchemaSequence sequence = complexType.ContentTypeParticle as XmlSchemaSequence;

                if (sequence != null)
                {
                    // Iterate over each XmlSchemaElement in the Items collection.
                    foreach (XmlSchemaObject childElement in sequence.Items)
                    {
                        if (childElement is XmlSchemaElement)
                        {
                            VerifyIfElementIsGlobal(concept, childElement);
                        }

                        if (childElement is XmlSchemaChoice)
                        {
                            foreach (XmlSchemaObject o in ((XmlSchemaChoice)childElement).Items)
                            {
                                VerifyIfElementIsGlobal(concept, o);
                            }

                        }

                        if (childElement is XmlSchemaSequence)
                        {
                            
                        }
                    }
                }
            }
        }


        private void VerifyIfElementIsGlobal(Concept concept, XmlSchemaObject childElement)
        {            
            if ((childElement as XmlSchemaElement).RefName.IsEmpty)
            {
                log.Error("Validation error on schema: In the declaration of any tuple in a taxonomy schema," +
                            "declarations of child elements of that tuple MUST be references to global " +
                            "element declarations that are in a substitution group that has either item " +
                            "or tuple as its head. Concept: " + concept.Name);
                dts.ValidatedSchema = false;
            }
        }


        private bool IsNamespaceDefault(string targetNamespace)
        {
            string namespacesDefault =
                                    "http://www.xbrl.org/2003/linkbase" + //link
                                    "http://www.xbrl.org/2003/instance" + //xbrli
                                    "http://www.xbrl.org/2003/XLink" + // xl
                                    "http://www.w3.org/1999/xlink" + //xlink
                                    "http://www.w3.org/XML/1998/namespace" + //xml
                                    "http://www.w3.org/2001/XMLSchema-instance" + //xsi
                                    "http://www.w3.org/2001/XMLSchema" + //xsd
                                    "http://xbrl.org/2008/generic" + // gen
                                    "http://xbrl.org/2008/formula" + // formula
                                    "http://xbrl.org/2008/assertion/consistency" + // ca
                                    "http://xbrl.org/2008/assertion/existence" + // ea
                                    "http://xbrl.org/2008/assertion/value" + // va
                                    "http://xbrl.org/2008/filter/concept" + // cf
                                    "http://xbrl.org/2008/label" + // label
                                    "http://xbrl.org/2010/message" + // msg
                                    "http://xbrl.org/2010/message/validation" + // validation
                                    "http://xbrl.org/2008/formula"; // formula

            if (namespacesDefault.Contains(targetNamespace))
            {
                return true;
            }

            return false;
        }

    }

        
}
